/* * Copyright (C) 2014 Jason M. Heim * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.jasonmheim.rollout.location; import android.app.Application; import android.app.PendingIntent; import android.content.ContentProviderClient; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.location.Location; import android.os.RemoteException; import android.util.Log; import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.location.FusedLocationProviderApi; import com.google.android.gms.location.LocationRequest; import com.jasonmheim.rollout.Constants; import java.util.concurrent.ExecutorService; import javax.inject.Inject; import javax.inject.Provider; import javax.inject.Singleton; import static com.jasonmheim.rollout.Constants.MS_PER_MINUTE; import static java.lang.Math.min; /** * Manages the last known location of the device. The incoming value should normally be set by the * {@link LocationUpdateIntentService}, and all updates in turn notify the central content provider. */ @Singleton public class LocationManager { static final String LATITUDE = "LastLatitude"; static final String LONGITUDE = "LastLongitude"; static final String TIMESTAMP = "LastLocationTimestamp"; private final Application application; private final ContentResolver contentResolver; private final ExecutorService executorService; private final FusedLocationProviderApi fusedLocationProviderApi; private final Provider<GoogleApiClient> locationClientProvider; private final SharedPreferences sharedPreferences; @Inject LocationManager( Application application, ContentResolver contentResolver, ExecutorService executorService, FusedLocationProviderApi fusedLocationProviderApi, Provider<GoogleApiClient> locationClientProvider, SharedPreferences sharedPreferences) { this.application = application; this.contentResolver = contentResolver; this.executorService = executorService; this.fusedLocationProviderApi = fusedLocationProviderApi; this.locationClientProvider = locationClientProvider; this.sharedPreferences = sharedPreferences; } /** * Update the current location and notify the central content provider so that interested parties * can be updated. Intended for use only by {@link LocationUpdateIntentService}, as such this is * private to the package. */ void setLastLocation(Location location) { Log.i("Rollout", "Storing location " + location.getLatitude() + " " + location.getLongitude()); // TODO: Using shared preferences for this is a bit of a hack. It will trigger anything that // listens for a change to the settings even though no actual settings are changed. This may // unintentionally consume double cycles if something is listening to both a settings change and // a content provider update. sharedPreferences.edit() .putString(LATITUDE, Double.toString(location.getLatitude())) .putString(LONGITUDE, Double.toString(location.getLongitude())) .putLong(TIMESTAMP, location.getTime()) .apply(); notifyContentProvider(); } /** * Retrieves the last recorded location, or {@code null} if it has never been known. This is * retrieved from storage rather than a GMS location client, and thus may be quite old, so callers * should be sure to check the time that the location was recorded if recency is important. */ public Location getLastLocation() { // TODO: consider adding synchronization or document why it's unnecessary. if (sharedPreferences.contains(LATITUDE) && sharedPreferences.contains(LONGITUDE)) { Location location = new Location(""); location.setLatitude(Double.parseDouble(sharedPreferences.getString(LATITUDE, "0"))); location.setLongitude(Double.parseDouble(sharedPreferences.getString(LONGITUDE, "0"))); location.setTime(sharedPreferences.getLong(TIMESTAMP, 0)); return location; } return null; } private void notifyContentProvider() { // TODO: use LOCATION_URI instead of STATION_URI; drop the content values. ContentValues contentValues = new ContentValues(); contentValues.put(Constants.UPDATE_KEY_LOCATION, true); ContentProviderClient contentProviderClient = contentResolver.acquireContentProviderClient(Constants.STATION_URI); try { contentProviderClient.update(Constants.STATION_URI, contentValues, null, null); } catch (RemoteException ex) { Log.e("Rollout", "Failed to update after new action"); } finally { contentProviderClient.release(); } } /** * Submits a request to set the location update interval and accuracy. Typically this is done when * the user has updated their action. Set to slow values for idle/silent actions, and fast values * for search/ride actions. */ public void setLocationUpdateInterval(final double intervalInMinutes, final int accuracy) { Log.i("Rollout", "Location update interval in minutes: " + intervalInMinutes); final long intervalInMs = (long) (intervalInMinutes * MS_PER_MINUTE); // The "fastest interval" should be less than the interval. Set it to the lesser of one minute // or half the requested interval. long halfIntervalInMs = intervalInMs / 2; final long fastestIntervalInMs = min(halfIntervalInMs, MS_PER_MINUTE); // Since we can't guarantee that the location client will be connected, run this on an executor // where we can perform a blocking connect to the location client. executorService.submit(new Runnable() { @Override public void run() { LocationRequest locationRequest = new LocationRequest() .setFastestInterval(fastestIntervalInMs) .setInterval(intervalInMs) .setPriority(accuracy); Context context = application.getApplicationContext(); Intent updateIntent = new Intent(context, LocationUpdateIntentService.class); PendingIntent pendingIntent = PendingIntent.getService( context, 0, updateIntent, PendingIntent.FLAG_UPDATE_CURRENT); GoogleApiClient locationClient = locationClientProvider.get(); locationClient.blockingConnect(); fusedLocationProviderApi.requestLocationUpdates( locationClient, locationRequest, pendingIntent); } }); } }